<template>
    <el-input
            class="el-date-editor"
            :class="'el-date-editor--' + type"
            :readonly="!editable || readonly || type === 'dates' || type === 'week'"
            :disabled="pickerDisabled"
            :size="pickerSize"
            :name="name"
            v-bind="firstInputId"
            v-if="!ranged"
            v-clickoutside="handleClose"
            :placeholder="placeholder"
            @focus="handleFocus"
            @keydown.native="handleKeydown"
            :value="displayValue"
            @input="value => userInput = value"
            @change="handleChange"
            @mouseenter.native="handleMouseEnter"
            @mouseleave.native="showClose = false"
            :validateEvent="false"
            ref="reference">
        <i slot="prefix"
           class="el-input__icon"
           :class="triggerClass"
           @click="handleFocus">
        </i>
        <i slot="suffix"
           class="el-input__icon"
           @click="handleClickIcon"
           :class="[showClose ? '' + clearIcon : '']"
           v-if="haveTrigger">
        </i>
    </el-input>
    <div
            class="el-date-editor el-range-editor el-input__inner"
            :class="[
      'el-date-editor--' + type,
      pickerSize ? `el-range-editor--${ pickerSize }` : '',
      pickerDisabled ? 'is-disabled' : '',
      pickerVisible ? 'is-active' : ''
    ]"
            @click="handleRangeClick"
            @mouseenter="handleMouseEnter"
            @mouseleave="showClose = false"
            @keydown="handleKeydown"
            ref="reference"
            v-clickoutside="handleClose"
            v-else>
        <i :class="['el-input__icon', 'el-range__icon', triggerClass]"></i>
        <input
                autocomplete="off"
                :placeholder="startPlaceholder"
                :value="displayValue && displayValue[0]"
                :disabled="pickerDisabled"
                v-bind="firstInputId"
                :readonly="!editable || readonly"
                :name="name && name[0]"
                @input="handleStartInput"
                @change="handleStartChange"
                @focus="handleFocus"
                class="el-range-input">
        <slot name="range-separator">
            <span class="el-range-separator">{{ rangeSeparator }}</span>
        </slot>
        <input
                autocomplete="off"
                :placeholder="endPlaceholder"
                :value="displayValue && displayValue[1]"
                :disabled="pickerDisabled"
                v-bind="secondInputId"
                :readonly="!editable || readonly"
                :name="name && name[1]"
                @input="handleEndInput"
                @change="handleEndChange"
                @focus="handleFocus"
                class="el-range-input">
        <i
                @click="handleClickIcon"
                v-if="haveTrigger"
                :class="[showClose ? '' + clearIcon : '']"
                class="el-input__icon el-range__close-icon">
        </i>
    </div>
</template>

<script>
    import Vue from 'vue';
    import Clickoutside from 'element-ui/src/utils/clickoutside';
    import {formatDate, parseDate, isDateObject, getWeekNumber} from 'element-ui/src/utils/date-util';
    import Popper from 'element-ui/src/utils/vue-popper';
    import Emitter from 'element-ui/src/mixins/emitter';
    import ElInput from 'element-ui/packages/input';
    import merge from 'element-ui/src/utils/merge';

    const NewPopper = {
        props: {
            appendToBody: Popper.props.appendToBody,
            offset: Popper.props.offset,
            boundariesPadding: Popper.props.boundariesPadding,
            arrowOffset: Popper.props.arrowOffset
        },
        methods: Popper.methods,
        data() {
            return merge({visibleArrow: true}, Popper.data);
        },
        beforeDestroy: Popper.beforeDestroy
    };

    const DEFAULT_FORMATS = {
        date: 'yyyy-MM-dd',
        month: 'yyyy-MM',
        datetime: 'yyyy-MM-dd HH:mm:ss',
        time: 'HH:mm:ss',
        week: 'yyyywWW',
        timerange: 'HH:mm:ss',
        daterange: 'yyyy-MM-dd',
        monthrange: 'yyyy-MM',
        datetimerange: 'yyyy-MM-dd HH:mm:ss',
        year: 'yyyy'
    };
    const HAVE_TRIGGER_TYPES = [
        'date',
        'datetime',
        'time',
        'time-select',
        'week',
        'month',
        'year',
        'daterange',
        'monthrange',
        'timerange',
        'datetimerange',
        'dates'
    ];
    const DATE_FORMATTER = function (value, format) {
        if (format === 'timestamp') return value.getTime();
        return formatDate(value, format);
    };
    const DATE_PARSER = function (text, format) {
        if (format === 'timestamp') return new Date(Number(text));
        return parseDate(text, format);
    };
    const RANGE_FORMATTER = function (value, format) {
        if (Array.isArray(value) && value.length === 2) {
            const start = value[0];
            const end = value[1];

            if (start && end) {
                return [DATE_FORMATTER(start, format), DATE_FORMATTER(end, format)];
            }
        }
        return '';
    };
    const RANGE_PARSER = function (array, format, separator) {
        if (!Array.isArray(array)) {
            array = array.split(separator);
        }
        if (array.length === 2) {
            const range1 = array[0];
            const range2 = array[1];

            return [DATE_PARSER(range1, format), DATE_PARSER(range2, format)];
        }
        return [];
    };
    const TYPE_VALUE_RESOLVER_MAP = {
        default: {
            formatter(value) {
                if (!value) return '';
                return '' + value;
            },
            parser(text) {
                if (text === undefined || text === '') return null;
                return text;
            }
        },
        week: {
            formatter(value, format) {
                let week = getWeekNumber(value);
                let month = value.getMonth();
                const trueDate = new Date(value);
                if (week === 1 && month === 11) {
                    trueDate.setHours(0, 0, 0, 0);
                    trueDate.setDate(trueDate.getDate() + 3 - (trueDate.getDay() + 6) % 7);
                }
                let date = formatDate(trueDate, format);

                date = /WW/.test(date)
                    ? date.replace(/WW/, week < 10 ? '0' + week : week)
                    : date.replace(/W/, week);
                return date;
            },
            parser(text, format) {
                // parse as if a normal date
                return TYPE_VALUE_RESOLVER_MAP.date.parser(text, format);
            }
        },
        date: {
            formatter: DATE_FORMATTER,
            parser: DATE_PARSER
        },
        datetime: {
            formatter: DATE_FORMATTER,
            parser: DATE_PARSER
        },
        daterange: {
            formatter: RANGE_FORMATTER,
            parser: RANGE_PARSER
        },
        monthrange: {
            formatter: RANGE_FORMATTER,
            parser: RANGE_PARSER
        },
        datetimerange: {
            formatter: RANGE_FORMATTER,
            parser: RANGE_PARSER
        },
        timerange: {
            formatter: RANGE_FORMATTER,
            parser: RANGE_PARSER
        },
        time: {
            formatter: DATE_FORMATTER,
            parser: DATE_PARSER
        },
        month: {
            formatter: DATE_FORMATTER,
            parser: DATE_PARSER
        },
        year: {
            formatter: DATE_FORMATTER,
            parser: DATE_PARSER
        },
        number: {
            formatter(value) {
                if (!value) return '';
                return '' + value;
            },
            parser(text) {
                let result = Number(text);

                if (!isNaN(text)) {
                    return result;
                } else {
                    return null;
                }
            }
        },
        dates: {
            formatter(value, format) {
                return value.map(date => DATE_FORMATTER(date, format));
            },
            parser(value, format) {
                return (typeof value === 'string' ? value.split(', ') : value)
                    .map(date => date instanceof Date ? date : DATE_PARSER(date, format));
            }
        }
    };
    const PLACEMENT_MAP = {
        left: 'bottom-start',
        center: 'bottom',
        right: 'bottom-end'
    };

    const parseAsFormatAndType = (value, customFormat, type, rangeSeparator = '-') => {
        if (!value) return null;
        const parser = (
            TYPE_VALUE_RESOLVER_MAP[type] ||
            TYPE_VALUE_RESOLVER_MAP['default']
        ).parser;
        const format = customFormat || DEFAULT_FORMATS[type];
        return parser(value, format, rangeSeparator);
    };

    const formatAsFormatAndType = (value, customFormat, type) => {
        if (!value) return null;
        const formatter = (
            TYPE_VALUE_RESOLVER_MAP[type] ||
            TYPE_VALUE_RESOLVER_MAP['default']
        ).formatter;
        const format = customFormat || DEFAULT_FORMATS[type];
        return formatter(value, format);
    };

    /*
     * Considers:
     *   1. Date object
     *   2. date string
     *   3. array of 1 or 2
     */
    const valueEquals = function (a, b) {
        // considers Date object and string
        const dateEquals = function (a, b) {
            const aIsDate = a instanceof Date;
            const bIsDate = b instanceof Date;
            if (aIsDate && bIsDate) {
                return a.getTime() === b.getTime();
            }
            if (!aIsDate && !bIsDate) {
                return a === b;
            }
            return false;
        };

        const aIsArray = a instanceof Array;
        const bIsArray = b instanceof Array;
        if (aIsArray && bIsArray) {
            if (a.length !== b.length) {
                return false;
            }
            return a.every((item, index) => dateEquals(item, b[index]));
        }
        if (!aIsArray && !bIsArray) {
            return dateEquals(a, b);
        }
        return false;
    };

    const isString = function (val) {
        return typeof val === 'string' || val instanceof String;
    };

    const validator = function (val) {
        // either: String, Array of String, null / undefined
        return (
            val === null ||
            val === undefined ||
            isString(val) ||
            (Array.isArray(val) && val.length === 2 && val.every(isString))
        );
    };

    export default {
        mixins: [Emitter, NewPopper],

        inject: {
            elForm: {
                default: ''
            },
            elFormItem: {
                default: ''
            }
        },

        props: {
            size: String,
            format: String,
            valueFormat: String,
            readonly: Boolean,
            placeholder: String,
            startPlaceholder: String,
            endPlaceholder: String,
            prefixIcon: String,
            clearIcon: {
                type: String,
                default: 'el-icon-circle-close'
            },
            name: {
                default: '',
                validator
            },
            disabled: Boolean,
            clearable: {
                type: Boolean,
                default: true
            },
            id: {
                default: '',
                validator
            },
            popperClass: String,
            editable: {
                type: Boolean,
                default: true
            },
            align: {
                type: String,
                default: 'left'
            },
            value: {},
            defaultValue: {},
            defaultTime: {},
            rangeSeparator: {
                default: '-'
            },
            pickerOptions: {},
            unlinkPanels: Boolean,
            validateEvent: {
                type: Boolean,
                default: true
            }
        },

        components: {ElInput},

        directives: {Clickoutside},

        data() {
            return {
                pickerVisible: false,
                showClose: false,
                userInput: null,
                valueOnOpen: null, // value when picker opens, used to determine whether to emit change
                unwatchPickerOptions: null
            };
        },

        watch: {
            pickerVisible(val) {
                if (this.readonly || this.pickerDisabled) return;
                if (val) {
                    this.showPicker();
                    this.valueOnOpen = Array.isArray(this.value) ? [...this.value] : this.value;
                } else {
                    this.hidePicker();
                    this.emitChange(this.value);
                    this.userInput = null;
                    if (this.validateEvent) {
                        this.dispatch('ElFormItem', 'el.form.blur');
                    }
                    this.$emit('blur', this);
                    this.blur();
                }
            },
            parsedValue: {
                immediate: true,
                handler(val) {
                    if (this.picker) {
                        this.picker.value = val;
                    }
                }
            },
            defaultValue(val) {
                // NOTE: should eventually move to jsx style picker + panel ?
                if (this.picker) {
                    this.picker.defaultValue = val;
                }
            },
            value(val, oldVal) {
                if (!valueEquals(val, oldVal) && !this.pickerVisible && this.validateEvent) {
                    this.dispatch('ElFormItem', 'el.form.change', val);
                }
            }
        },

        computed: {
            ranged() {
                return this.type.indexOf('range') > -1;
            },

            reference() {
                const reference = this.$refs.reference;
                return reference.$el || reference;
            },

            refInput() {
                if (this.reference) {
                    return [].slice.call(this.reference.querySelectorAll('input'));
                }
                return [];
            },

            valueIsEmpty() {
                const val = this.value;
                if (Array.isArray(val)) {
                    for (let i = 0, len = val.length; i < len; i++) {
                        if (val[i]) {
                            return false;
                        }
                    }
                } else {
                    if (val) {
                        return false;
                    }
                }
                return true;
            },

            triggerClass() {
                return this.prefixIcon || (this.type.indexOf('time') !== -1 ? 'el-icon-time' : 'el-icon-date');
            },

            selectionMode() {
                if (this.type === 'week') {
                    return 'week';
                } else if (this.type === 'month') {
                    return 'month';
                } else if (this.type === 'year') {
                    return 'year';
                } else if (this.type === 'dates') {
                    return 'dates';
                }

                return 'day';
            },

            haveTrigger() {
                if (typeof this.showTrigger !== 'undefined') {
                    return this.showTrigger;
                }
                return HAVE_TRIGGER_TYPES.indexOf(this.type) !== -1;
            },

            displayValue() {
                const formattedValue = formatAsFormatAndType(this.parsedValue, this.format, this.type, this.rangeSeparator);
                if (Array.isArray(this.userInput)) {
                    return [
                        this.userInput[0] || (formattedValue && formattedValue[0]) || '',
                        this.userInput[1] || (formattedValue && formattedValue[1]) || ''
                    ];
                } else if (this.userInput !== null) {
                    return this.userInput;
                } else if (formattedValue) {
                    return this.type === 'dates'
                        ? formattedValue.join(', ')
                        : formattedValue;
                } else {
                    return '';
                }
            },

            parsedValue() {
                if (!this.value) return this.value; // component value is not set
                if (this.type === 'time-select') return this.value; // time-select does not require parsing, this might change in next major version

                const valueIsDateObject = isDateObject(this.value) || (Array.isArray(this.value) && this.value.every(isDateObject));
                if (valueIsDateObject) {
                    return this.value;
                }

                if (this.valueFormat) {
                    return parseAsFormatAndType(this.value, this.valueFormat, this.type, this.rangeSeparator) || this.value;
                }

                // NOTE: deal with common but incorrect usage, should remove in next major version
                // user might provide string / timestamp without value-format, coerce them into date (or array of date)
                return Array.isArray(this.value) ? this.value.map(val => new Date(val)) : new Date(this.value);
            },

            _elFormItemSize() {
                return (this.elFormItem || {}).elFormItemSize;
            },

            pickerSize() {
                return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
            },

            pickerDisabled() {
                return this.disabled || (this.elForm || {}).disabled;
            },

            firstInputId() {
                const obj = {};
                let id;
                if (this.ranged) {
                    id = this.id && this.id[0];
                } else {
                    id = this.id;
                }
                if (id) obj.id = id;
                return obj;
            },

            secondInputId() {
                const obj = {};
                let id;
                if (this.ranged) {
                    id = this.id && this.id[1];
                }
                if (id) obj.id = id;
                return obj;
            }
        },

        created() {
            // vue-popper
            this.popperOptions = {
                boundariesPadding: 0,
                gpuAcceleration: false
            };
            this.placement = PLACEMENT_MAP[this.align] || PLACEMENT_MAP.left;

            this.$on('fieldReset', this.handleFieldReset);
        },

        methods: {
            focus() {
                if (!this.ranged) {
                    this.$refs.reference.focus();
                } else {
                    this.handleFocus();
                }
            },

            blur() {
                this.refInput.forEach(input => input.blur());
            },

            // {parse, formatTo} Value deals maps component value with internal Date
            parseValue(value) {
                const isParsed = isDateObject(value) || (Array.isArray(value) && value.every(isDateObject));
                if (this.valueFormat && !isParsed) {
                    return parseAsFormatAndType(value, this.valueFormat, this.type, this.rangeSeparator) || value;
                } else {
                    return value;
                }
            },

            formatToValue(date) {
                const isFormattable = isDateObject(date) || (Array.isArray(date) && date.every(isDateObject));
                if (this.valueFormat && isFormattable) {
                    return formatAsFormatAndType(date, this.valueFormat, this.type, this.rangeSeparator);
                } else {
                    return date;
                }
            },

            // {parse, formatTo} String deals with user input
            parseString(value) {
                const type = Array.isArray(value) ? this.type : this.type.replace('range', '');
                return parseAsFormatAndType(value, this.format, type);
            },

            formatToString(value) {
                const type = Array.isArray(value) ? this.type : this.type.replace('range', '');
                return formatAsFormatAndType(value, this.format, type);
            },

            handleMouseEnter() {
                if (this.readonly || this.pickerDisabled) return;
                if (!this.valueIsEmpty && this.clearable) {
                    this.showClose = true;
                }
            },

            handleChange() {
                if (this.userInput) {
                    const value = this.parseString(this.displayValue);
                    if (value) {
                        this.picker.value = value;
                        if (this.isValidValue(value)) {
                            this.emitInput(value);
                            this.userInput = null;
                        }
                    }
                }
                if (this.userInput === '') {
                    this.emitInput(null);
                    this.emitChange(null);
                    this.userInput = null;
                }
            },

            handleStartInput(event) {
                if (this.userInput) {
                    this.userInput = [event.target.value, this.userInput[1]];
                } else {
                    this.userInput = [event.target.value, null];
                }
            },

            handleEndInput(event) {
                if (this.userInput) {
                    this.userInput = [this.userInput[0], event.target.value];
                } else {
                    this.userInput = [null, event.target.value];
                }
            },

            handleStartChange(event) {
                const value = this.parseString(this.userInput && this.userInput[0]);
                if (value) {
                    this.userInput = [this.formatToString(value), this.displayValue[1]];
                    const newValue = [value, this.picker.value && this.picker.value[1]];
                    this.picker.value = newValue;
                    if (this.isValidValue(newValue)) {
                        this.emitInput(newValue);
                        this.userInput = null;
                    }
                }
            },

            handleEndChange(event) {
                const value = this.parseString(this.userInput && this.userInput[1]);
                if (value) {
                    this.userInput = [this.displayValue[0], this.formatToString(value)];
                    const newValue = [this.picker.value && this.picker.value[0], value];
                    this.picker.value = newValue;
                    if (this.isValidValue(newValue)) {
                        this.emitInput(newValue);
                        this.userInput = null;
                    }
                }
            },

            handleClickIcon(event) {
                if (this.readonly || this.pickerDisabled) return;
                if (this.showClose) {
                    this.valueOnOpen = this.value;
                    event.stopPropagation();
                    this.emitInput(null);
                    this.emitChange(null);
                    this.showClose = false;
                    if (this.picker && typeof this.picker.handleClear === 'function') {
                        this.picker.handleClear();
                    }
                } else {
                    this.pickerVisible = !this.pickerVisible;
                }
            },

            handleClose() {
                if (!this.pickerVisible) return;
                this.pickerVisible = false;

                if (this.type === 'dates') {
                    // restore to former value
                    const oldValue = parseAsFormatAndType(this.valueOnOpen, this.valueFormat, this.type, this.rangeSeparator) || this.valueOnOpen;
                    this.emitInput(oldValue);
                }
            },

            handleFieldReset(initialValue) {
                this.userInput = initialValue === '' ? null : initialValue;
            },

            handleFocus() {
                const type = this.type;

                if (HAVE_TRIGGER_TYPES.indexOf(type) !== -1 && !this.pickerVisible) {
                    this.pickerVisible = true;
                }
                this.$emit('focus', this);
            },

            handleKeydown(event) {
                const keyCode = event.keyCode;

                // ESC
                if (keyCode === 27) {
                    this.pickerVisible = false;
                    event.stopPropagation();
                    return;
                }

                // Tab
                if (keyCode === 9) {
                    if (!this.ranged) {
                        this.handleChange();
                        this.pickerVisible = this.picker.visible = false;
                        this.blur();
                        event.stopPropagation();
                    } else {
                        // user may change focus between two input
                        setTimeout(() => {
                            if (this.refInput.indexOf(document.activeElement) === -1) {
                                this.pickerVisible = false;
                                this.blur();
                                event.stopPropagation();
                            }
                        }, 0);
                    }
                    return;
                }

                // Enter
                if (keyCode === 13) {
                    if (this.userInput === '' || this.isValidValue(this.parseString(this.displayValue))) {
                        this.handleChange();
                        this.pickerVisible = this.picker.visible = false;
                        this.blur();
                    }
                    event.stopPropagation();
                    return;
                }

                // if user is typing, do not let picker handle key input
                if (this.userInput) {
                    event.stopPropagation();
                    return;
                }

                // delegate other keys to panel
                if (this.picker && this.picker.handleKeydown) {
                    this.picker.handleKeydown(event);
                }
            },

            handleRangeClick() {
                const type = this.type;

                if (HAVE_TRIGGER_TYPES.indexOf(type) !== -1 && !this.pickerVisible) {
                    this.pickerVisible = true;
                }
                this.$emit('focus', this);
            },

            hidePicker() {
                if (this.picker) {
                    this.picker.resetView && this.picker.resetView();
                    this.pickerVisible = this.picker.visible = false;
                    this.destroyPopper();
                }
            },

            showPicker() {
                if (this.$isServer) return;
                if (!this.picker) {
                    this.mountPicker();
                }
                this.pickerVisible = this.picker.visible = true;

                this.updatePopper();

                this.picker.value = this.parsedValue;
                this.picker.resetView && this.picker.resetView();

                this.$nextTick(() => {
                    this.picker.adjustSpinners && this.picker.adjustSpinners();
                });
            },

            mountPicker() {
                this.picker = new Vue(this.panel).$mount();
                this.picker.defaultValue = this.defaultValue;
                this.picker.defaultTime = this.defaultTime;
                this.picker.popperClass = this.popperClass;
                this.popperElm = this.picker.$el;
                this.picker.width = this.reference.getBoundingClientRect().width;
                this.picker.showTime = this.type === 'datetime' || this.type === 'datetimerange';
                this.picker.selectionMode = this.selectionMode;
                this.picker.unlinkPanels = this.unlinkPanels;
                this.picker.arrowControl = this.arrowControl || this.timeArrowControl || false;
                this.$watch('format', (format) => {
                    this.picker.format = format;
                });

                const updateOptions = () => {
                    const options = this.pickerOptions;

                    if (options && options.selectableRange) {
                        let ranges = options.selectableRange;
                        const parser = TYPE_VALUE_RESOLVER_MAP.datetimerange.parser;
                        const format = DEFAULT_FORMATS.timerange;

                        ranges = Array.isArray(ranges) ? ranges : [ranges];
                        this.picker.selectableRange = ranges.map(range => parser(range, format, this.rangeSeparator));
                    }

                    for (const option in options) {
                        if (options.hasOwnProperty(option) &&
                            // 忽略 time-picker 的该配置项
                            option !== 'selectableRange') {
                            this.picker[option] = options[option];
                        }
                    }

                    // main format must prevail over undocumented pickerOptions.format
                    if (this.format) {
                        this.picker.format = this.format;
                    }
                };
                updateOptions();
                this.unwatchPickerOptions = this.$watch('pickerOptions', () => updateOptions(), {deep: true});
                this.$el.appendChild(this.picker.$el);
                this.picker.resetView && this.picker.resetView();

                this.picker.$on('dodestroy', this.doDestroy);
                this.picker.$on('pick', (date = '', visible = false) => {
                    this.userInput = null;
                    this.pickerVisible = this.picker.visible = visible;
                    this.emitInput(date);
                    this.picker.resetView && this.picker.resetView();
                });

                this.picker.$on('select-range', (start, end, pos) => {
                    if (this.refInput.length === 0) return;
                    if (!pos || pos === 'min') {
                        this.refInput[0].setSelectionRange(start, end);
                        this.refInput[0].focus();
                    } else if (pos === 'max') {
                        this.refInput[1].setSelectionRange(start, end);
                        this.refInput[1].focus();
                    }
                });
            },

            unmountPicker() {
                if (this.picker) {
                    this.picker.$destroy();
                    this.picker.$off();
                    if (typeof this.unwatchPickerOptions === 'function') {
                        this.unwatchPickerOptions();
                    }
                    this.picker.$el.parentNode.removeChild(this.picker.$el);
                }
            },

            emitChange(val) {
                // determine user real change only
                if (!valueEquals(val, this.valueOnOpen)) {
                    this.$emit('change', val);
                    this.valueOnOpen = val;
                    if (this.validateEvent) {
                        this.dispatch('ElFormItem', 'el.form.change', val);
                    }
                }
            },

            emitInput(val) {
                const formatted = this.formatToValue(val);
                if (!valueEquals(this.value, formatted)) {
                    this.$emit('input', formatted);
                }
            },

            isValidValue(value) {
                if (!this.picker) {
                    this.mountPicker();
                }
                if (this.picker.isValidValue) {
                    return value && this.picker.isValidValue(value);
                } else {
                    return true;
                }
            }
        }
    };
</script>
